/**
 * Copyright 2012 ibiblio
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.ibiblio.terasaur.bt

import org.ibiblio.terasaur.bt.TorrentMetainfoException
import org.ibiblio.terasaur.bencode.ByteArray
import org.ibiblio.terasaur.bencode.Bencode
import java.nio.ByteBuffer

// for sha-1
import java.io.UnsupportedEncodingException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.Formatter

class TorrentMetainfo {
    private static _bencode = new Bencode()

    public String created_by // optional
    public String announce
    public List announce_list // optional
    public String encoding // optional
    public String comment // optional
    public Integer creation_date // optional
    public TorrentMetainfoInfoDictionary info
    public String info_hash // derived (hash of bencoded info data)
    public Long size // derived (sum of file sizes)

    /**
     * Input Map should be the bdecoded data of a torrent file.  If keepPieces
     * is true, make an internal copy of the piece hash array.  Otherwise,
     * the pieces array is ignored.
     *
     * @param torrentData
     * @param keepPieces
     */
    TorrentMetainfo(Map torrentData, boolean keepPieces=false) {
        this._validate(torrentData)
        this.created_by = torrentData.get('created by')
        this.announce = torrentData.get('announce')
        if (torrentData.containsKey('announce-list')) {
            this.announce_list = []
            torrentData.get('announce-list').each { this.announce_list << it[0].toString() }
        }
        this.encoding = torrentData.get('encoding')
        this.comment = torrentData.get('comment')
        this.creation_date = torrentData.get('creation date')
        if (torrentData.containsKey('info')) {
            def torrentInfo = torrentData.get('info')
            this.info = new TorrentMetainfoInfoDictionary(torrentInfo, keepPieces)
            this.info_hash = this._calculateInfohash(torrentInfo)
            this.size = this._calculateSize(this.info)
        }
    }

    private void _validate(Map torrentData) {
        if (!torrentData.containsKey('announce')) {
            throw new Exception('Missing announce metainfo key')
        }
    }

    protected String _calculateInfohash(HashMap info) throws TorrentMetainfoException, NoSuchAlgorithmException, UnsupportedEncodingException  {
        byte[] infoData = _bencode.encode(info).toByteArray()
        MessageDigest md = MessageDigest.getInstance("SHA1")
        md.update(infoData, 0, infoData.length)
        ByteArray sha1hash = new ByteArray(md.digest())
        return this.toHex(sha1hash)
    }

    /**
     * Calculate the content size of the torrent by adding the file lengths
     * in the file list.
     *
     * @param TorrentMetainfoInfoDictionary info
     * @return Long
     */
    protected Long _calculateSize(TorrentMetainfoInfoDictionary info) {
        Long s
        if (info.length) {
            // single file
            s = info.length
        } else {
            // multiple files
            s = 0
            this.info.files.each {
                s += it.length
            }
        }
        return s
    }

    public String toString() {
        def s = "TorrentMetainfo [\n"
        s += '    info_hash: ' + this.info_hash + "\n"
        s += '    created_by: ' + this.created_by + "\n"
        s += '    announce: ' + this.announce + "\n"
        s += '    announce_list: ' + this.announce_list + "\n"
        s += '    encoding: ' + this.encoding + "\n"
        s += '    comment: ' + this.comment + "\n"
        s += '    creation_date: ' + this.creation_date + "\n"
        if (this.info == null) {
            s += "    info: null\n"
        } else {
            s += this.info.toString()
        }
        s += "]\n"
        return s
    }

    public static String toHex(ByteArray bytes) {
        Formatter formatter = new Formatter();
        for (byte b : bytes.getArray()) {
            formatter.format("%02x", b);
        }
        return formatter.toString();
    }

    public static ByteArray toBinary(String hex) {
        int hexlen = hex.length()
        ByteBuffer buffer = ByteBuffer.allocate((int)(hexlen/2))
        int idx = 0
        String byteHex
        int byteInt

        while (idx < hexlen) {
            byteHex = hex[idx..idx+1]
            byteInt = Integer.parseInt(byteHex, 16);
            buffer.put((byte)byteInt)
            idx += 2
        }

        ByteArray bytes = new ByteArray()
        bytes.setBytes(buffer.array())
        return bytes
    }

    public List getFileList() {
        def fileList = []
        def fileItem
        def filePath
        if (this.info.length) {
            // single file
            fileItem = [
                'path': this.info.name,
                'length': this.info.length]
            fileList << fileItem
        } else {
            // multiple files
            def fileLength
            this.info.files.each {
                fileItem = [
                    'path': this.info.name + File.separator + it.path,
                    'length': it.length]
                fileList << fileItem
            }
        }
        return fileList
    }

    public List getAnnounceList() {
        def announceList = []

        announceList << this.announce
        this.announce_list.each() {
            announceList << it
        }
        announceList.unique()
        return announceList
    }

    public ByteArrayOutputStream encode() {
        Map data = toMap()
        return _bencode.encode(data)
    }

    private Map toMap() {
        Map data = [:]
        if (this.created_by) {
            data['created by'] = new ByteArray(this.created_by.bytes)
        }
        data.announce = new ByteArray(this.announce.bytes)
        if (this.announce_list) {
            data['announce-list'] = []
            this.announce_list.each { data['announce-list'] << [new ByteArray(it.bytes)] }
        }
        if (this.encoding) {
            data.encoding = new ByteArray(this.encoding.bytes)
        }
        if (this.comment) {
            data.comment = new ByteArray(this.comment.bytes)
        }
        data['creation date'] = (Long)this.creation_date
        data.info = this.info.toMap()
        return data
    }
}
